package com.szeldon.pj.jps.envs;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import com.szeldon.pj.jps.data.store.SBAStore;
import com.szeldon.pj.jps.datastore.ComplexObject;
import com.szeldon.pj.jps.datastore.ReferenceObject;
import com.szeldon.pj.jps.datastore.SBAObject;
import com.szeldon.pj.jps.datastore.SBAObjectId;
import com.szeldon.pj.jps.datastore.SimpleObject;
import com.szeldon.pj.jps.qres.AbstractQueryResult;
import com.szeldon.pj.jps.qres.BagResult;
import com.szeldon.pj.jps.qres.BinderResult;
import com.szeldon.pj.jps.qres.BooleanResult;
import com.szeldon.pj.jps.qres.IntegerResult;
import com.szeldon.pj.jps.qres.RealResult;
import com.szeldon.pj.jps.qres.ReferenceResult;
import com.szeldon.pj.jps.qres.StringResult;
import com.szeldon.pj.jps.qres.StructResult;

public class ENVS {

	private static final Logger logger = Logger.getLogger(ENVS.class.getCanonicalName());

	private List<ENVSStackFrame> stack;

	/**
	 * 
	 * @param rootId
	 * @param store
	 */
	public void init(SBAObjectId rootId, SBAStore store) {
		ENVSStackFrame rootFrame = new ENVSStackFrame();
		SBAObject rootSBAObject = store.retrieve(rootId);
		if (!(rootSBAObject instanceof ComplexObject)) {
			throw new IllegalStateException("Root (entry) object must be of " + ComplexObject.class.getName()
					+ " type.");
		}
		ComplexObject rootObject = (ComplexObject) rootSBAObject;
		for (SBAObjectId currentSubobjectId : rootObject.getSubobjectsList()) {
			SBAObject currentSubobject = store.retrieve(currentSubobjectId);
			if (currentSubobject == null) {
				throw new IllegalStateException("Datastore returns no object for SBAObjectId=" + currentSubobjectId);
			}
			ENVSBinder binder = new ENVSBinder(currentSubobject.getName(), new ReferenceResult(currentSubobjectId));
			rootFrame.add(binder);
		}

		stack = new ArrayList<ENVSStackFrame>();
		stack.add(rootFrame);
	}

	public ENVSStackFrame pop() {
		ENVSStackFrame removedFrame = stack.remove(stack.size() - 1);
		logger.info("popping frame=" + removedFrame);
		return removedFrame;
	}

	public void push(ENVSStackFrame frame) {
		logger.info("pushing frame=" + frame);
		stack.add(frame);
	}

	/**
	 * Binds given name with SBAObjects. Binded SBAObjects are returned in a bag.
	 * 
	 * @param name
	 * @return
	 */
	public BagResult bind(String name) {
		logger.info("binding \"" + name + "\"");
		if (stack == null) {
			throw new IllegalStateException("ENVS not initialized before binding.");
		}

		BagResult bagResult = null;
		for (int i = stack.size() - 1; i >= 0; i--) {
			ENVSStackFrame currentFrame = stack.get(i);
			bagResult = currentFrame.findElementsByName(name);
			if (bagResult != null) {
				break;
			}
		}
		if (bagResult == null) {
			throw new IllegalStateException("\"" + name + "\" name can't be binded to any object.");
		}
		logger.info("binded \"" + name + "\" with bag: " + bagResult);
		return bagResult;
	}

	/**
	 * Returns binders for given result.
	 * 
	 * @param binders
	 * @param store
	 * @return
	 */
	public List<ENVSBinder> nested(AbstractQueryResult result, SBAStore store) {
		logger.info("invoking nested(" + result + ")");
		if (stack == null) {
			throw new IllegalStateException("ENVS not initialized before nesting.");
		}
		List<ENVSBinder> binders = null;

		if (result instanceof ReferenceResult) {
			binders = handleReferenceResult((ReferenceResult) result, store);
		} else if (result instanceof BinderResult) {
			binders = handleBinderResult((BinderResult) result, store);
		} else if (result instanceof BagResult) {
			binders = handleBagResult((BagResult) result, store);
		} else if (result instanceof StructResult) {
			throw new IllegalStateException("TODO: Currently nested operation in ENVS doesn't support "
					+ result.getClass().getSimpleName() + " as an argument. It should change in a while.");
		} else if (result instanceof BooleanResult || result instanceof IntegerResult || result instanceof RealResult
				|| result instanceof StringResult) {
			binders = new ArrayList<ENVSBinder>();
		} else {
			// nothing
		}

		logger.info("invoked nested(" + result + ")=" + binders);
		return binders;
	}

	private List<ENVSBinder> handleBagResult(BagResult result, SBAStore store) {
		ArrayList<ENVSBinder> binders = new ArrayList<ENVSBinder>();

		for (AbstractQueryResult currentResult : result.getElements()) {
			List<ENVSBinder> currentResultBinders = nested(currentResult, store);
			if (currentResultBinders != null) {
				binders.addAll(currentResultBinders);
			}
		}

		return binders;
	}

	private List<ENVSBinder> handleBinderResult(BinderResult result, SBAStore store) {
		ArrayList<ENVSBinder> binders = new ArrayList<ENVSBinder>();

		binders.add(new ENVSBinder(result.getName(), result.getValue()));

		return binders;
	}

	private List<ENVSBinder> handleReferenceResult(ReferenceResult result, SBAStore store) {
		SBAObjectId referenceObjectId = result.getReferencedObjectId();
		SBAObject referencedObject = store.retrieve(referenceObjectId);
		if (referencedObject == null) {
			throw new IllegalStateException("SBAObject with id=" + referenceObjectId + " doesn't exist in store.");
		}

		ArrayList<ENVSBinder> binders = new ArrayList<ENVSBinder>();

		if (referencedObject instanceof ComplexObject) {
			ComplexObject referencedComplexObject = (ComplexObject) referencedObject;
			for (SBAObjectId currentSubobjectId : referencedComplexObject.getSubobjectsList()) {
				SBAObject currentSubobject = store.retrieve(currentSubobjectId);
				binders.add(new ENVSBinder(currentSubobject.getName(), new ReferenceResult(currentSubobjectId)));
			}
		} else if (referencedObject instanceof ReferenceObject) {
			binders.add(new ENVSBinder(referencedObject.getName(), new ReferenceResult(referenceObjectId)));
		} else if (referencedObject instanceof SimpleObject) {
			// nothing
		} else {
			throw new IllegalStateException("Unexpected state. Reference result contains reference to "
					+ referencedObject.getClass().getName() + " object which isn't supported.");
		}

		return binders;
	}

}
